<?php

namespace Swof\Container;

use Closure;
use Exception;
use phpDocumentor\Reflection\Types\Mixed_;

class Container
{
    /**
     * The current globally available container (if any).
     *
     * @var static
     */
    protected static $instance = null;

    /**
     * An array of the types that have been resolved.
     *
     * @var bool[]
     */
    protected $resolved = [];

    /**
     * The container's bindings.
     *
     * @var array
     */
    protected $bindings = [];

    /**
     * The container's shared instances.
     *
     * @var object[]
     */
    protected $instances = [];

    /**
     * Register a binding with the container.
     *
     * @param string $abstract
     * @param Closure|string|null $concrete
     * @param bool $shared
     * @return void
     * @throws Exception
     */
    public function bind(string $abstract, $concrete = null, bool $shared = false)
    {
        $this->dropStaleInstances($abstract);

        if (is_null($concrete)) {
            $concrete = $abstract;
        }

        $this->bindings[$abstract] = compact('concrete', 'shared');

        if ($this->resolved($abstract)) {
            $this->rebound($abstract);
        }
    }

    /**
     * Fire the "rebound" callbacks for the given abstract type.
     *
     * @param string $abstract
     * @return void
     * @throws Exception
     */
    protected function rebound(string $abstract)
    {
        $this->make($abstract);
    }

    /**
     * Drop all of the stale instances and aliases.
     *
     * @param  string  $abstract
     * @return void
     */
    protected function dropStaleInstances(string $abstract)
    {
        unset($this->instances[$abstract]);
    }

    /**
     * Register an existing instance as shared in the container.
     *
     * @param  string  $abstract
     * @param  mixed  $instance
     * @return mixed
     */
    public function instance(string $abstract, $instance)
    {
        $this->instances[$abstract] = $instance;

        return $instance;
    }

    /**
     * Register a shared binding in the container.
     *
     * @param string $abstract
     * @param Closure|string|null $concrete
     * @return void
     * @throws Exception
     */
    public function singleton(string $abstract, $concrete = null)
    {
        $this->bind($abstract, $concrete, true);
    }

    /**
     * Note: 获取当前类的实例
     *
     * @return Container|static
     * @author: Zhao Zhiqiang
     * @since: 2021/5/16 9:33
     */
    public static function getInstance(): Container
    {
        if (is_null(static::$instance)) {
            static::$instance = new static;
        }

        return static::$instance;
    }

    /**
     * Note: 设置
     *
     * @param mixed|null $container
     * @author: Zhao Zhiqiang
     * @since: 2021/5/16 9:34
     */
    public static function setInstance($container = null): Container
    {
        return static::$instance = $container;
    }

    /**
     * Note: 解决
     *
     * @param string $abstract
     * @param array $parameters
     * @return mixed
     * @since: 2021/5/16 9:53
     * @author: Zhao Zhiqiang
     */
    public function make(string $abstract, array $parameters = [])
    {
        return $this->resolve($abstract, $parameters);
    }

    /**
     * Note:
     *
     * @param string $abstract
     * @return bool
     * @author: Zhao Zhiqiang
     * @since: 2021/5/13 9:51
     */
    public function has(string $abstract): bool
    {
        return isset($this->bindings[$abstract]);
    }

    /**
     * Determine if the given abstract type has been resolved.
     *
     * @param  string  $abstract
     * @return bool
     */
    public function resolved(string $abstract): bool
    {
        return isset($this->resolved[$abstract]) ||
            isset($this->instances[$abstract]);
    }

    /**
     * Note: Resolve the given type from the container.
     *
     * @param string $abstract
     * @param array $parameters
     * @return callable|mixed|object
     * @author: Zhao Zhiqiang
     * @since: 2021/5/26 15:04
     */
    protected function resolve(string $abstract, array $parameters = [])
    {
        if (isset($this->instances[$abstract])) {
            return $this->instances[$abstract];
        }

        $object = $this->getConcrete($abstract);

        if ($object instanceof Closure) {
            return $object($this);
        }

        $object = (is_object($object)) ? $object :  new $object(...$parameters);

        $this->instances[$abstract] = $object;

        return $object;
    }

    /**
     * Get the concrete type for a given abstract.
     *
     * @param  string|callable  $abstract
     * @return mixed
     */
    protected function getConcrete($abstract)
    {
        // If we don't have a registered resolver or concrete for the type, we'll just
        // assume each type is a concrete name and will attempt to resolve it as is
        // since the container should be able to resolve concretes automatically.
        if (isset($this->bindings[$abstract])) {
            return $this->bindings[$abstract]['concrete'];
        }

        return $abstract;
    }

    /**
     * Remove a resolved instance from the instance cache.
     *
     * @param  string  $abstract
     * @return void
     */
    public function forgetInstance(string $abstract)
    {
        unset($this->instances[$abstract]);
    }

    /**
     * Clear all of the instances from the container.
     *
     * @return void
     */
    public function forgetInstances()
    {
        $this->instances = [];
    }

    /**
     * Flush the container of all bindings and resolved instances.
     *
     * @return void
     */
    public function flush()
    {
        $this->resolved = [];
        $this->bindings = [];
        $this->instances = [];
    }

    /**
     * Dynamically access container services.
     *
     * @param string $key
     * @return mixed
     */
    public function __get(string $key)
    {
        return $this[$key];
    }

    /**
     * Dynamically set container services.
     *
     * @param string $key
     * @param  mixed  $value
     * @return void
     */
    public function __set(string $key, $value)
    {
        $this[$key] = $value;
    }
}
